﻿using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using Newtonsoft.Json;
using SlickGridNet.Serialization;
using SlickGridNet.Utils;

namespace SlickGridNet
{
	public partial class SlickGrid : IDisposable
	{
		/// <summary>
		/// Default name for SlickGrid.
		/// </summary>
		public const string DefaultName = "SleekyGrid";

		private JsonSerializer _jsonSerializer;

		/// <summary>
		/// Columns collections
		/// </summary>
		private readonly List<SlickColumn> _columns = new List<SlickColumn>();

		/// <summary>
		/// scripts collection
		/// </summary>
		private readonly ResourcesCollection _scripts = new ResourcesCollection();
		/// <summary>
		/// stylesheets collection
		/// </summary>
		private readonly ResourcesCollection _stylesheets = new ResourcesCollection();

		/// <summary>
		/// SlickGrid name.
		/// </summary>
		private string _name;

		private SlickOptions _options;
		private string _containerSelector;
		private string _optionsName;
		private string _columnsName;
		private string _dataName;

		/// <summary>
		/// collection of self realized objects
		/// </summary>
		private readonly Dictionary<Type, ISelfRealize> _selfRealizeOptions = new Dictionary<Type, ISelfRealize>();

		#region Properties

		/// <summary>
		/// Name of SlickGrid object that will be used in rendering process for composint identifiers of Javascript variables.
		/// </summary>
		public string Name
		{
			get { return _name; }
			set
			{
				if (value == null)
					throw new ArgumentNullException("value", "Name of SlickGrid object can't be null.");
				if (!StringUtils.IsIdentifier(value))
					throw new ArgumentException("Name of SlickGrid should conform to Javascript variable name rules.", "value");
				_name = value;

				if (OptionsName == null)
					OptionsName = _name + "Options";
				if (ColumnsName == null)
					ColumnsName = _name + "Columns";
				if (DataName == null)
					DataName = _name + "Data";
			}
		}

		/// <summary>
		/// JS selector for HTML container for SlickGrid
		/// </summary>
		public string ContainerSelector
		{
			get { return _containerSelector; }
			set
			{
				if (value == null)
					throw new ArgumentNullException("value");
				_containerSelector = value;
			}
		}

		/// <summary>
		/// Name of options variable.
		/// </summary>
		public string OptionsName
		{
			get { return _optionsName; }
			set
			{
				if (value == null)
					throw new ArgumentNullException("value");
				if (!StringUtils.IsIdentifier(value))
					throw new ArgumentException("Options variable name should conform to Javascript variable name rules.", "value");
				_optionsName = value;
			}
		}

		/// <summary>
		/// Name of columns variable.
		/// </summary>
		public string ColumnsName
		{
			get { return _columnsName; }
			set
			{
				if (value == null)
					throw new ArgumentNullException("value");
				if (!StringUtils.IsIdentifier(value))
					throw new ArgumentException("Columns variable name should conform to Javascript variable name rules.", "value");
				_columnsName = value;
			}
		}

		/// <summary>
		/// Name of data variable.
		/// </summary>
		public string DataName
		{
			get { return _dataName; }
			set
			{
				if (value == null)
					throw new ArgumentNullException("value");
				if (!StringUtils.IsIdentifier(value))
					throw new ArgumentException("Data variable name should conform to Javascript variable name rules.", "value");
				_dataName = value;
			}
		}

		/// <summary>
		/// SlickGrid options.
		/// </summary>
		/// <exception cref="ArgumentNullException">Exeption thrown if null value set.</exception>
		public SlickOptions Options
		{
			get { return _options; }
			set
			{
				if (value == null)
					throw new ArgumentNullException("value", "Options can't be null.");
				_options = value;
			}
		}

		/// <summary>
		/// Columns collection.
		/// </summary>
		public List<SlickColumn> Columns
		{
			get { return _columns; }
		}

		public object Data { get; set; }

		#endregion


		#region Constructors

		/// <summary>
		/// Creates new instance of <see cref="SlickGrid"/> with default name.
		/// </summary>
		public SlickGrid()
			: this(DefaultName)
		{}

		/// <summary>
		/// Creates new instance of <see cref="SlickGrid"/> with specified name.
		/// </summary>
		/// <param name="name">Name of creating SlickGrid object.</param>
		public SlickGrid(string name)
			: this(name, "#" + name)
		{}

		public SlickGrid(string name, string containerSelector)
		{
			if (name == null)
				throw new ArgumentNullException("name", "Name of SlickGrid object can't be null.");
			Name = name;

			if (containerSelector == null)
				throw new ArgumentNullException("containerSelector");
			_containerSelector = containerSelector;
		}

		/// <summary>
		/// Creates new instance of <see cref="SlickGrid"/> using optional params of SlickGrid options and SlickGrid columns.
		/// </summary>
		/// <param name="options">Optional parameter, specifies options for slick grid.
		/// <para>If it's not specified or specified as <c>null</c> <see cref="SlickOptions"/> object will be created.</para></param>
		/// <param name="columns">Optional parameter, specifies columns range for SlickGrid.</param>
		public SlickGrid(SlickOptions options = null, IEnumerable<SlickColumn> columns = null, object data = null)
			: this(DefaultName, options, columns, data)
		{}

		/// <summary>
		/// Creates new instance of <see cref="SlickGrid"/> using optional params of SlickGrid options and SlickGrid columns.
		/// </summary>
		/// <param name="name">Name of creating SlickGrid object.</param>
		/// <param name="options">Optional parameter, specifies options for slick grid.
		/// <para>If it's not specified or specified as <c>null</c> <see cref="SlickOptions"/> object will be created.</para></param>
		/// <param name="columns">Optional parameter, specifies columns range for SlickGrid.</param>
		public SlickGrid(string name, SlickOptions options = null, IEnumerable<SlickColumn> columns = null, object data = null)
			: this(name, "#" + name, options, columns, data)
		{}

		public SlickGrid(string name, string containerSelector,
			SlickOptions options = null, IEnumerable<SlickColumn> columns = null, object data = null)
			: this(name, containerSelector)
		{
			Options = options ?? new SlickOptions();

			if (columns != null)
				_columns.AddRange(columns);

			Data = data;
		}

		#endregion Constructors


		/// <summary>
		/// Add specified column to SlickGrid columns list.
		/// </summary>
		/// <param name="column">Instance of SlickColumn type.</param>
		/// <returns>Returns this SlickGrid object for pipeline operations.</returns>
		public SlickGrid AddColumn(SlickColumn column)
		{
			_columns.Add(column);
			return this;
		}

		/// <summary>
		/// Adds range of column to columns collection.
		/// </summary>
		/// <param name="columns">IEnumerable collection of SlickColumn to add.</param>
		/// <returns>Returns this SlickGrid object for pipeline operations.</returns>
		public SlickGrid AddColumns(IEnumerable<SlickColumn> columns)
		{
			if (columns == null)
				throw new ArgumentNullException("columns");
			_columns.AddRange(columns);
			return this;
		}

		#region Renders

		protected JsonSerializer Serializer
		{
			get { return _jsonSerializer ?? (_jsonSerializer = JsonSerializer.Create(new JsonSerializerSettings())); }
		}

		#region Columns Render

		/// <summary>
		/// Renders columns into JavaScript collection without variable declaration.
		/// </summary>
		/// <returns>JSON representation of columns collection.</returns>
		public String RenderColumns()
		{
			return RenderColumns(false);
		}

		/// <summary>
		/// Renders columns into JavaScript collection without variable declaration.
		/// <para>Output can be formated - use parameter <paramref name="formatOutput"/></para>
		/// </summary> 
		/// <param name="formatOutput">If true - output will be formatted, 
		/// else it will be write in one line.</param>
		/// <returns>JSON representation of columns collection.</returns>
		public String RenderColumns(bool formatOutput)
		{
			var sb = new StringBuilder();

			using (var sw = new StringWriter(sb))
			using (var jtw = new JsonTextWriter(sw) {QuoteName = false}.SetFormatting(formatOutput))
			{
				RenderColumnsInternal(jtw);
			}

			return sb.ToString();
		}

		/// <summary>
		/// Renders columns as JavaScript collection without variable declaration 
		/// into stream specified with <paramref name="writer"/> parameter.
		/// <para>Output is not formatted by default.</para>
		/// </summary> 
		/// <param name="writer">Stream to write rendered string to.</param>
		public void RenderColumns(TextWriter writer)
		{
			RenderColumns(writer, false);
		}

		/// <summary>
		/// Renders columns into JavaScript collection without variable declaration
		/// into stream specified with <paramref name="writer"/> parameter.
		/// <para>Output can be formated - use parameter <paramref name="formatOutput"/></para>
		/// </summary>
		/// <param name="writer">Stream to write rendered string to.</param>
		/// <param name="formatOutput">If true - output will be formatted, 
		/// else it will be write in one line.</param>
		public void RenderColumns(TextWriter writer, bool formatOutput)
		{
			if (writer == null) throw new ArgumentNullException("writer", "Writer param can't be null.");
			using (var jtw = new JsonTextWriter(writer) {QuoteName = false, CloseOutput = false}.SetFormatting(formatOutput))
			{
				RenderColumnsInternal(jtw);
			}
		}

		protected void RenderColumnsInternal(JsonTextWriter jtw)
		{
			var jsonSerializer = Serializer;

			jsonSerializer.Serialize(jtw, _columns);
		}

		#endregion

		#region Options Render

		/// <summary>
		/// Renders options as JavaScript associatas array without variable declaration into 
		/// stream specified with <paramref name="writer"/> parameter.
		/// <para>Output not formated by default.</para>
		/// </summary>
		/// <param name="writer">Stream to write rendered string to.</param>
		public void RenderOptions(TextWriter writer)
		{
			RenderOptions(writer, false);
		}

		/// <summary>
		/// Renders options as JavaScript associatas array without variable declaration into 
		/// stream specified with <paramref name="writer"/> parameter. 
		/// </summary>
		/// <param name="writer">Stream to write rendered string to.</param>
		/// <param name="formatOutput">If <c>True</c> output will be formated (indented).</param>
		public void RenderOptions(TextWriter writer, bool formatOutput)
		{
			if (writer == null) throw new ArgumentNullException("writer", "Weriter param can't ve null.");

			using (var jtw = new JsonTextWriter(writer) {QuoteName = false, CloseOutput = false}.SetFormatting(formatOutput))
			{
				RenderOptionsInternal(jtw);
			}
		}

		/// <summary>
		/// Renders options as JavaScript associatas array without variable declaration 
		/// <para>Output not formated by default.</para>
		/// </summary>
		public string RenderOptions()
		{
			return RenderOptions(false);
		}

		/// <summary>
		/// Renders options as JavaScript associatas array without variable declaration. 
		/// </summary>
		/// <param name="formatOutput">If <c>True</c> output will be formated (indented).</param>
		public string RenderOptions(bool formatOutput)
		{
			var sb = new StringBuilder();
			using (var sw = new StringWriter(sb))
			using (var jtw = new JsonTextWriter(sw) {QuoteName = false}.SetFormatting(formatOutput))
			{
				RenderOptionsInternal(jtw);
			}
			return sb.ToString();
		}

		protected void RenderOptionsInternal(JsonTextWriter jtw)
		{
			var jsonSerializer = Serializer;

			Options.RenderInternal(jtw, jsonSerializer);
		}

		#endregion

		#region Self Realize Options Render

		/// <summary>
		/// Renders self realize objects as JavaScript text.
		/// </summary>
		/// <returns>JSON representation of columns collection.</returns>
		public String RenderIncludes()
		{
			var sb = new StringBuilder();

			using (var sw = new StringWriter(sb))
				RenderIncludes(sw);

			return sb.ToString();
		}

		/// <summary>
		/// Renders self realize objects as JavaScript text
		/// into stream specified with <paramref name="writer"/> parameter.
		/// </summary>
		/// <param name="writer">Stream to write rendered string to.</param>
		public void RenderIncludes(TextWriter writer)
		{
			if (writer == null) throw new ArgumentNullException("writer", "Writer param can't be null.");
			using (var jtw = new JsonTextWriter(writer) {QuoteName = false, CloseOutput = false})
			{
				RenderIncludesInternal(jtw);
			}
		}

		/// <summary>
		/// Renders self realize objects as JavaScript text.
		/// </summary>
		/// <param name="jtw">JsonTextWriter to write rendered objects to.</param>
		protected void RenderIncludesInternal(JsonTextWriter jtw)
		{
			var jsCodeSb = new StringBuilder();

			foreach (var column in _columns)
				foreach (
					var selfRealizeObject in
						column.SelfRealizeOptions.Where(selfRealizeObject => !_selfRealizeOptions.ContainsKey(selfRealizeObject.Key)))
				{
					_selfRealizeOptions.Add(selfRealizeObject.Key, selfRealizeObject.Value);

					selfRealizeObject.Value.IncludeScripts(_scripts);
					selfRealizeObject.Value.IncludeStylesheets(_stylesheets);

					// generating collection of self realize scripts
					jsCodeSb.AppendLine(selfRealizeObject.Value.GetCode());
				}

			foreach (ResourceItem stylesheet in _stylesheets)
			{
				jtw.WriteRaw(String.Format("<link rel=\"stylesheet\" type=\"text/css\" href=\"{0}\" />", stylesheet.AbsolutePath));
				jtw.WriteRaw(Environment.NewLine);
			}
			foreach (ResourceItem script in _scripts)
			{
				jtw.WriteRaw(String.Format("<script type=\"text/javascript\" src=\"{0}\"></script>", script.AbsolutePath));
				jtw.WriteRaw(Environment.NewLine);
			}

			if (jsCodeSb.Length != 0)
			{
				jtw.WriteRaw("<script type=\"text/javascript\" language=\"javascript\">");
				jtw.WriteRaw(Environment.NewLine);
				jtw.WriteRaw(jsCodeSb.ToString());
				jtw.WriteRaw("</script>");
			}
		}

		#endregion

		public String Render()
		{
			var sb = new StringBuilder();

			using (var sw = new StringWriter(sb))
			using (var jtw = new JsonTextWriter(sw) {QuoteName = false})
			{
				RenderIncludesInternal(jtw);

				jtw.WriteRaw("<script type=\"text/javascript\" language=\"javascript\">");
				jtw.WriteRaw(Environment.NewLine);

				jtw.WriteRaw("var " + Name + ";");
				jtw.WriteRaw(Environment.NewLine);

				jtw.WriteRaw("var " + ColumnsName + " = ");
				RenderColumnsInternal(jtw);
				jtw.WriteRaw(";");
				jtw.WriteRaw(Environment.NewLine);

				jtw.WriteRaw("var " + OptionsName + " = ");
				RenderOptionsInternal(jtw);
				jtw.WriteRaw(";");
				jtw.WriteRaw(Environment.NewLine);

				jtw.WriteRaw("var " + DataName + " = ");
				Serializer.Serialize(jtw, Data);
				jtw.WriteRaw(";");
				jtw.WriteRaw(Environment.NewLine);

				jtw.WriteRaw("$(function () {");
				jtw.WriteRaw(Environment.NewLine);
				jtw.WriteRaw(String.Format("\t{0} = new Slick.Grid(\"{1}\", {2}, {3}, {4});",
					Name, ContainerSelector, DataName, ColumnsName, OptionsName));
				jtw.WriteRaw(Environment.NewLine);
				jtw.WriteRaw("});");
				jtw.WriteRaw(Environment.NewLine);

				jtw.WriteRaw("</script>");
			}

			return sb.ToString();
		}

		#endregion

		public void Dispose()
		{}
	}

	internal class SlickGrid<TModel> : SlickGrid
	{}
}
